# data.frame to test
<- jsonlite::fromJSON(
arcs '[
{
"e": "CN",
"i": "US",
"v": 3300000
},
{
"e": "CN",
"i": "RU",
"v": 10000
}
]'
)
print(arcs)
#> e i v
#> 1 CN US 3300000
#> 2 CN RU 10000
07 The Full Monty
本章以gio.js库为基础,学习构建更加完备。gio.js用来在三维地球图上,通过曲线绘制国家之间的联系。下面是该库的一个简单示例:
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="" xml:lang="">
<head>
<!-- Import libraries -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/110/three.min.js"></script>
<script src="https://raw.githubusercontent.com/syt123450/giojs/master/build/gio.min.js"></script>
</head>
<body>
<!-- div to hold visualisation -->
<div id="globe" style="width: 200px; height: 200px"></div>
<!-- Script to create visualisation -->
<script>
// data is demo
var data = [
{"e": "CN",
"i": "US",
"v": 3300000
,
}
{"e": "CN",
"i": "RU",
"v": 10000
}
]var container = document.getElementById("globe");
var controller = new GIO.Controller(container);
.addData(data);
controller.init();
controller</script>
</body>
</html>
Creating Package
同上一章一样:
- 首先创建
gio
R包及scaffold:
::create_package("gio")
usethis::scaffoldWidget("gio") htmlwidgets
- 然后下载依赖JS库,修改
inst/htmlwidgets/gio.yaml
文件
# create directories for JS dependencies
dir.create("./inst/htmlwidgets/three", recursive = TRUE)
dir.create("./inst/htmlwidgets/gio", recursive = TRUE)
# download JS dependencies
<- paste0(
three "https://cdnjs.cloudflare.com/ajax/",
"libs/three.js/110/three.min.js"
)<- paste0(
gio "https://raw.githubusercontent.com/",
"syt123450/giojs/master/build/gio.min.js"
)
download.file(three, "./inst/htmlwidgets/three/three.min.js")
download.file(gio, "./inst/htmlwidgets/gio/gio.min.js")
dependencies:
- name: three
version: 110
src: htmlwidgets/three
script: three.min.js
- name: gio
version: 2.0
src: htmlwidgets/gio
script: gio.min.js
- 修改
int/htmlwidgets/gio.js
文件中的函数:因为gio.js直接使用widget创建的el对象,所以我们无需使用var container = document.getElementById("globe")
来获取el对象,而是直接使用el
对象;同时需要的数据由x对象传入。
// gio.js
.widget({
HTMLWidgets
name: 'gio',
type: 'output',
factory: function(el, width, height) {
// TODO: define shared variables for this instance
return {
renderValue: function(x) {
// var container = document.getElementById("globe");
var controller = new GIO.Controller(el);
.addData(x.data);
controller.init();
controller
,
}
resize: function(width, height) {
// TODO: code to re-render the widget with a new size
}
;
}
}; })
- 最后修改
R/gio.R
文件:上面的js需要输入对象x要有data
属性,所以这里需要修改gio
函数的参数和x
的数据结构。
# 注意:将message修改为data
<- function(data, width = NULL, height = NULL, elementId = NULL) {
gio
# forward options using x
= list(
x data = data
)
# create widget
::createWidget(
htmlwidgetsname = 'gio',
x,width = width,
height = height,
package = 'gio',
elementId = elementId
) }
- 最后运行
devtools::document();devtools::load_all()
加载函数,使用gio(data)
创建widget。
Working with Data
现在只需要我们创建gio.js需要的json数据,即可创建gio.js的widget。阅读jio.js的文档,其要求的数据结构为:e——exporting country, i——importing country, v——value。
[
{
"e": "CN",
"i": "US",
"v": 3300000
},
{
"e": "CN",
"i": "RU",
"v": 10000
}
]
我们使用R读取该JSON数据,会发现数据结构转为data.frame。
当我们直接将arcs
传入gio
函数时,会发现只有一个空白的widget。使用console.log
或查看HTML源码,会发现传入的数据结构和预期的不符。
{
"x":{
"data":{
"e":["CN","CN"],
"i":["US","RU"],
"v":[3300000,10000]
}
},
"evals":[],
"jsHooks":[]
}
出现上述情况的原因是:row-wise的JSON被jsonlite::fromJSON
自动转换为了dataframe,而htmlwidgets
的createWidget
函数会自动将dataframe转为column-wise的JSON,从而导致数据结构不符。
# column-wise
::toJSON(arcs, dataframe = "columns")
jsonlite#> {"e":["CN","CN"],"i":["US","RU"],"v":[3300000,10000]}
# row-wise
::toJSON(arcs, dataframe = "rows")
jsonlite#> [{"e":"CN","i":"US","v":3300000},{"e":"CN","i":"RU","v":10000}]
Transforming Data
下面介绍一些确保dataframe被row-wise的方法。
Using JavaScript
htmlwidgets JavaScript 库提供了dataframeToD3
函数,将column-wise的JSON转为row-wise的JSON。
// gio.js
: function(x) {
renderValue
// long to wide
.data = HTMLWidgets.dataframeToD3(x.data);
x
var controller = new GIO.Controller(el);
.addData(x.data);
controller.init();
controller
}
Modify Serialiser
正如前面讲到的那样,我们可以设置htmlwidgets
的createWidget
函数自动将dataframe转为row-wise的JSON。
下面是createWidget
函数的底层逻辑,可以看到只要修改dataframe
参数为"rows"
即可。
function (x, ..., dataframe = "columns", null = "null",
na = "null", auto_unbox = TRUE, use_signif = TRUE,
digits = getOption("shiny.json.digits", 16), force = TRUE,
POSIXt = "ISO8601", UTC = TRUE, rownames = FALSE,
keep_vec_names = TRUE, strict_atomic = TRUE)
{if (strict_atomic)
<- I(x)
x ::toJSON(x, dataframe = dataframe, null = null, na = na,
jsonliteauto_unbox = auto_unbox, digits = digits, force = force,
use_signif = use_signif, POSIXt = POSIXt, UTC = UTC,
rownames = rownames, keep_vec_names = keep_vec_names,
json_verbatim = TRUE, ...)
}
htmlwidgets巧妙地利用了属性,可以将rows
设置为x
对象的TOJSON_ARGS
属性,从而实现row-wise处理。
<- function(data, width = NULL, height = NULL,
gio elementId = NULL) {
# forward options using x
= list(
x data = data
)
# serialise data.frames to wide (not long as default)
attr(x, 'TOJSON_ARGS') <- list(dataframe = "rows")
# create widget
::createWidget(
htmlwidgetsname = 'gio',
x,width = width,
height = height,
package = 'gio',
elementId = elementId
) }
Replace Serialiser
也可以为x
对象添加TOJSON_FUNC
属性值,直接替换serialiser函数。
<- function(data, width = NULL, height = NULL,
gio elementId = NULL) {
# forward options using x
= list(
x data = data
)
# replace serialiser
attr(x, 'TOJSON_FUNC') <- gio_serialiser
# create widget
::createWidget(
htmlwidgetsname = 'gio',
x,width = width,
height = height,
package = 'gio',
elementId = elementId
)
}
# serialiser
<- function(x){
gio_serialiser ::to_json(x, unbox = TRUE)
jsonify }
Modify the Data
我们也可以直接修改data
数据类型。
= list(
x data = apply(data, 1, as.list)
)
Pros and Cons
上述每种方法都有其优缺点。最好的方法可能是仅在需要的地方修改默认序列化器(Modify Serialiser),这是本书其余部分使用的方法。完全替换序列化器(Replace Serialiser)应该是不必要的,只有在你非常熟悉序列化并真正看到需要它时才这样做。此外,HTMLWidgets 的序列化器扩展了 jsonlite, 允许转换 JavaScript 代码,这将在后面派上用场。在 JavaScript 中转换 data (Using JavaScript)有一个缺点,HTMLWidgets.dataframeToD3 不能应用于整个 x 对象,它只会作用于包含column-wise data (x.data) 的子集,这往往会导致代码笨拙,因为在不同地方使用该函数。
On Print Method
gio.js提供了一些themes,我们可以为其x
对象添加style`属性。
// gio.js
: function(x) {
renderValue
var controller = new GIO.Controller(el);
.addData(x.data);
controller
.setStyle(x.style); // set style
controller
.init();
controller
}
此时,我们除了前面讲到的修改R/gio.R
文件中函数的data
对象,还可以直接为x
对象添加style。使用print()
我们可以清楚地看到数据结构。
<- gio(arcs) # nothing renders
g # visualisation renders g
print(g$x)
#> $data
#> e i v
#> 1 CN US 3300000
#> 2 CN RU 10000
#>
#> attr(,"TOJSON_ARGS")
#> attr(,"TOJSON_ARGS")$dataframe
#> [1] "rows"
在R/gio.R
文件中,创建添加属性的函数:
#' @export
<- function(g, style = "magic"){
gio_style $x$style <- style
greturn(g)
}
<- gio(arcs)
g1 <- gio_style(g1, "juicyCake")
g2
g2
htmlwidgets系列包也可以被magrittr包支持,使用usethis::use_pipe()
可以方便的实现管道符操作。
library(magrittr)
gio(arcs) %>%
gio_style("juicyCake")